Galileo Computing <openbook>
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net

...powered by www.netzwerkartist.de...

Java 2 von Friedrich Esser
Designmuster und Zertifizierungswissen
Zum Katalog
gp Kapitel 7 Ausnahmen
  gp 7.1 Konzeption
  gp 7.2 Ausnahme-Mechanismus
  gp 7.3 Details zur Ausnahme-Behandlung
  gp 7.4 Ausnahme-Kategorien in Java
  gp 7.5 Overriding von Ausnahmen
  gp 7.6 Deklaration neuer Ausnahmen
  gp 7.7 Einsatz von Ausnahmen
  gp 7.8 Ausnahmen-Behandlung
  gp 7.9 Zusammenfassung
  gp 7.10 Testfragen

Kapitel 7 Ausnahmen

Methoden müssen auf Laufzeitfehler, die bei der Programmausführung auftreten, geeignet reagieren, entweder selbst oder indem sie die Ursache und die Kontrolle an die aufrufende Methode weiterreichen. Diesen Kontrollmechanismus nennt man in Java Ausnahmebehandlung.
Die Behandlung von Laufzeitfehlern ist in der Java-Plattform auf Basis einer differenzierten Hierarchie von Ausnahme- bzw. Exception-Klassen gelöst.
Der effiziente Einsatz von Ausnahmen zum Design robuster Klassen ist ergo das zentrale Thema.

Compiler-Fehler
vs. Laufzeitfehler

Fehler können anhand des Zeitpunkts ihres Auftretens unterschieden werden: Fehler treten schon bei der Übersetzung oder erst zur Laufzeit auf.

Sicherlich sind Fehler, die der Compiler entdeckt, die angenehmeren. Man kann sie analysieren und den Code entsprechend anpassen.

Im ungünstigeren Fall treten Laufzeitfehler erst beim Anwender – dem Kunden – auf und beenden das Programm ohne geeignete Behandlung, lassen es »abstürzen«.

Die Behandlung der Laufzeitfehler macht bei interaktiven Programmen häufig den Hauptteil des Codes aus. Die produktiven Programmteile sind mit unzähligen Kontrollanweisungen durchsetzt, die auf widrige Umweltsituationen reagieren müssen.

Laufzeitfehler werden häufig ignoriert!

Bei traditionellen Sprachen wie C besteht das Übel darin, dass Fehler, die an eine aufrufende Methode als Ergebnis oder Parameter weitergereicht werden, durchaus ignoriert werden können.

Dies führt dann zu dem noch unangenehmeren Phänomen, dass die Absturzursache unklar bleibt.

Ausnahmen können nicht ignoriert werden

In Java begegnet man diesem Laissez-faire-Verhalten mit der Erzeugung und dem Auslösen von Ausnahme-Objekten , wenn nötig, durch die JVM selbst.

Diese Ausnahme-Objekte, einmal auf den Weg gebracht, müssen im Programm abgefangen werden oder erzeugen nette Konsol-Meldungen.


Galileo Computing

7.1 Konzeption  downtop

Bei der Konzeption neuer Sprachen geht man davon aus, dass eine konventionelle Behandlung der meisten Laufzeitfehler nicht genügt.

Icon

Die wesentlichen Design-Anforderungen sind

Forderungen an die Behandlung von Laufzeitfehlern

gp  minimal-invasiv: Eine möglichst geringe (negative) Auswirkung auf den normalen Programmablauf (normal flow of control).
gp  Klarheit: Konzentration der Fehlerbehandlung auf klar erkennbare Programmabschnitte.
gp  Erweiterbarkeit: Je nach Programmumgebung treten andere, neue Laufzeitfehler auf, die in das System integiert werden müssen.

Unterscheidung von Laufzeitfehlern

Auch Laufzeitfehler können je nach Art oder Ursache in Kategorien unterteilt werden. Im Sprachverständnis von Java signalisieren Laufzeitfehler zwei unterschiedliche Arten von Ausnahme-Situationen:

Fehlerhafter Code

gp  fehlerhaften Programm-Code, der verbessert werden muss

Unpassende Stelle

gp  Fehler, die dort, wo sie auftreten, nicht sinnvoll behandelt werden können

Zum ersten Fall: Der Programm-Code muss so verbessert werden, dass diese Fehler nicht mehr auftreten können.

Zum zweiten Fall: Der normale Programmablauf muss zwangsläufig unterbrochen werden und der Fehler an einer »angemesseneren« Stelle behandelt werden. Eine Rückkehr an den Punkt des Auftretens wird ausgeschlossen.

Java-Konzeption:
Es gibt kein Zurück
(not recoverable)

Das Konzept von Java ist stringent und durchaus diskussionswürdig. Es steht im Gegensatz zu Konzepten, die auch Laufzeitfehler in den normalen Kontrollfluss einbinden können.

Visual Basic – ich bitte um Nachsicht – unterscheidet u.a. leichte Fehler, die erst einmal ignoriert werden können (On Error Resume Next). Schwere Fehler müssen zwar sofort behandelt werden, aber erlauben es durchaus, anschließend wieder an der Stelle fortzufahren, an der sie aufgetreten sind.


Galileo Computing

7.2 Ausnahme-Mechanismus  downtop

Basisklasse
Throwable

In Java sollen Ausnahmen immer Instanzen von Subklassen der Klasse java.lang.Throwable sein. Bei einem Laufzeitfehler muss man das Auslösen einer Ausnahme vom Behandeln der Ausnahme unterscheiden.


Galileo Computing

7.2.1 Ausnahme-Auslösung (exception throwing)  downtop

Wer löst Ausnahmen aus?

Eine Ausnahme kann wie folgt ausgelöst werden:

gp  durch die JVM
gp  explizit durch eine Anweisung

Ausnahme-Behandler (exception handler)

Die Ausnahme ist immer eine Instanz einer Ausnahme-Klasse, die den entsprechenden Laufzeitfehler identifiziert und wird von der JVM an einen passenden Ausnahme-Behandler weitergereicht (siehe 7.2.2).

throw:
explizites Auslösen einer Ausnahme

Explizit wird eine Ausnahme durch

          throw throwableExpression;

ausgelöst, wobei throwableExpression eine Instanz vom (Sub-)Typ Throwable ergeben muss.

Beispiele

In der folgenden Methode wird eine Ausnahme durch die JVM ausgelöst, wenn der Divisor Null ist:

Implizites Auslösen einer Ausnahme

static int intDiv (int i, int j) {
  return i/j;  // bei einer Ausnahme erfolgt kein return
}

Hat j den Wert 0, erzeugt die JVM eine Instanz der Klasse ArithmeticException und aktiviert die Ausnahme.

In der nächsten Methode wird bei einem null-Argument eine Instanz von NullPointerException erschaffen und mittels throw ausgelöst:

Explizites Auslösen einer Ausnahme

class Point { int x,y; /*...*/ 
}
static double distanceFromOrigin (Point p) {
  if (p==null)
    throw new java.lang.NullPointerException("p ist null");
  return Math.sqrt(p.x*p.x+p.y*p.y);
}

Bei diesen beiden Methoden wird die return-Anweisung beim Auftreten einer Ausnahme nicht mehr ausgeführt.


Galileo Computing

7.2.2 Ausnahme-Behandlung (exception handlingdowntop

Icon

Die Behandlung einer Ausnahme erfolgt nach generellen Regeln:

Unbehandelte Ausnahme

1. Wird eine Ausnahme ausgelöst, gilt sie als unbehandelt und führt zum sofortigen Abbruch des normalen Programmablaufs.

Such-Muster
der JVM

2. Die JVM sucht als Nächstes im umgebenden Code bzw. in den aufrufenden Methoden so lange nach einer try-Anweisung, bis sie einen passenden catch-Block findet oder aber die oberste Methode der Threads, main() oder run(), verlassen muss.

Unbehandelte Ausnahme: Abbruch!

3. Das Verlassen des obersten Methoden-Blocks führt bei einer unbehandelten Ausnahme zum Abbruch (der Thread).

try behandelt
Ausnahmen

4. Nur eine try-Anweisung kann Ausnahmen behandeln:
      try { tryStatementSequenz }
    [ catch (ExceptionType1 e1) { exceptionHandlerSequenz1 }
      ...
      catch (ExceptionTypeN eN) { exceptionHandlerSequenzN } ]
    [ finally { finallyStatementSequenz } ]
Dem try-Block muss zumindest ein optionaler catch- oder finally-Block folgen.

catch-Behandlung von Ausnahmen

5. Sofern catch-Blöcke vorhanden sind,
    gp  werden nacheinander die einzigen Parameter ExceptionTypeX darauf untersucht, ob die unbehandelte Ausnahme-Instanz ein (Sub-) Typ ist.
    gp  wird bei einem passenden ExceptionTypeX der zugehörige catch-Block ausgeführt, alle nachfolgenden catch-Blöcke übersprungen und die Ausnahme gilt als behandelt.
    gp  kann hierdurch erneut eine Ausnahme ausgelöst werden.

finally ist
unabhängig von Ausnahmen

6. Sofern ein finally-Block vorhanden ist,
    gp  wird er immer am Ende der try-Anweisung ausgeführt, egal, ob eine Ausnahme aufgetreten, behandelt oder nicht behandelt ist.

Lost Exception!

    gp  kann eine unbehandelte Ausnahme durch eine return-Anweisung »verloren” gehen oder aber durch die Auslösung einer neuen Ausnahme ersetzt werden.

Ausnahme
unbehandelt? Propagieren!

7. Wurde die Ausnahme behandelt und keine neue ausgelöst, wird die Ausführung nach der try-Anweisung fortgesetzt. Ansonsten wiederholt sich die Suche, beginnend mit dem zweiten Schritt.

Ausnahmen können also nur mit Hilfe von catch-Ausnahme-Parametern erkannt und behandelt werden.

Der finally-Block kann nur indirekt Ausnahmen durch Überschreiben oder Missachtung entfernen, was durchaus zu Problemen führen kann (siehe hierzu 7.3.3).

Aktivitäts-Diagramm (UML)

Aktivitäts-Diagramme:
Darstellung von Prozessen in UML

Aktivitäts-Diagramme wurden in Kapitel 4, Modellierung und UML, zwar nicht formal vorgestellt, sind aber zur Darstellung von Prozessabläufen und Bearbeitungsschritten recht reizvoll.

Start und Ende des Ablaufs sind unschwer an den massiven Kreisen zu erkennen. Die Bearbeitung (Aktivität) wird in Rechtecken dargestellt und folgt den Pfeilen und Entscheidungsrauten, wobei die jeweilige Bedingung (guard) in eckigen Klammern erfüllt sein muss.

Objekte, die beteiligt sind, und Kommentare runden das Diagramm ab.

Aktivitäts-Diagramm
zur Behandlung von Ausnahmen


Abbildung
Abbildung 7.1   Behandlung von Ausnahmen vom Typ Exception

In Abb. 7.1 wird der Versuch unternommen, das generelle Muster der Ausnahmebehandlung – analog zu den genannten Bearbeitungsschritten – anhand eines Aktivitäts-Diagramms darzustellen.

Beispiele

Auslösen einer NullPointerException

Zuerst wird in der Methode distanceFromOrigin() (siehe 7.2.1) eine NullPointerException ausgelöst:

double d= 0.0;
try { 
  d= distanceFromOrigin(null); 
  System.out.println(d);        // wird nicht ausgeführt     ¨
}  
catch (Exception e) { d= Double.NaN; }
finally             { System.out.println(d); }  // :: NaN

Exception-Handling
2. Schritt

Erklärung: Die Ausnahme wird in distanceFromOrigin() ausgelöst und nicht behandelt. Also sucht die JVM im umgebenden Block.

4. Schritt

Die Suche endet in der try-Anweisung, die die Methode enthält.

1. Schritt

Diese wird sofort abgebrochen, ohne println() in Zeile ¨ auszuführen.

5. Schritt

Da NullPointerException eine Subklasse von Exception ist, ist der einzige catch-Block der passende Ausnahme-Behandler und die Ausnahme gilt als behandelt.

6. Schritt

Abschließend wird noch der finally-Block ausgeführt.

Variationen:
catch-Blöcke, finally-Block

Die folgende Klasse Test ist aufgrund ihrer verschiedenen Versionen interessant:

public class Test {
  public static void main(String[] args) {
    int[] iarr= {0,1,2};
    int i=0;
  /* Version 1 */
    try { while(true) System.out.println(1/iarr[i++]); }
    catch (IndexOutOfBoundsException e) {
      System.out.println("Ind");
    }
 /* Version 2
    catch (ArithmeticException e) { 
      System.out.println("Ari");
    } */
 /* Version 3
    finally { System.out.println("fin");
    } */
  }
}

Exception-Handling
2. Schritt

Version 1 (nur erster catch-Block): In der while-Schleife tritt sofort eine ArithmeticException auf. Da ArithmeticException keine Subklasse von IndexOutOfBoundsException ist, passt der erste catch-Block nicht. Die JVM muss die Applikation Test abbrechen.

5. Schritt

Version 2 (inkl. zweitem catch-Block): Nun gibt es einen passenden catch-Block. Die Ausnahme gilt als behandelt und das Programm beendet normal (:: Ari).

6. Schritt

Version 3 (nur erster catch-Block und finally): Es gibt zwar keinen passenden catch-Block, allerdings wird vor dem Programmabbruch erst finally ausgeführt (:: fin).


Galileo Computing

7.3 Details zur Ausnahme-Behandlung  downtop

Es gibt noch einige interessante Details, die in den Bearbeitungsschritten (siehe 7.2.2) noch nicht angesprochen wurden.


Galileo Computing

7.3.1 catch-Reihenfolge  downtop

Icon

Folgen einem try-Block mehrere catch-Blöcke, muss aufgrund der sequenziellen Abarbeitung der catch-Blöcke folgende Reihenfolge eingehalten werden:

Reihenfolge der
catch-Exceptions

gp  catch-Blöcke mit spezielleren Ausnahmen müssen vor catch-Blöcken mit generellen Ausnahmen angegeben werden.

Der Compiler prüft die Einhaltung der Regel und erzeugt ggf. eine »unreachable«-Fehlermeldung:

Nach dem fünften Schritt in 7.2.2 verdeckt ein generellerer Ausnahmetyp alle speziellen, d.h. alle nachfolgenden Subtypen, die somit nicht ausgeführt werden könnten.

Das kleine Beispiel »Test« provoziert eine Fehlermeldung:

public class Test {
  public static void main(String[] args) {

Compiler erzeugt »unreachable« -Meldung

    try { System.out.println(1/0); }
    catch (Exception e) { }                      // C-Fehler
    catch (ArithmeticException e) { }    
  }
}

Ein Vertauschen der beiden catch-Blöcke löst das Problem.


Galileo Computing

7.3.2 Geschachtelte Ausnahmen  downtop

try-Anweisungen schachteln

Ausnahme-Behandlungen können ineinander geschachtelt werden, was sehr angenehm sein kann.

        try-block          
            try-block      
               try-block  
               ...
            catch-blocks  
            finally-block
        catch-blocks     
        finally-block   

Ausführung von geschachtelten try-Anweisungen

Gemäß dem fünften Schritt in 7.2.2 werden bei Auslösen einer inneren Ausnahme zuerst alle zugehörigen inneren catch-Blöcke durchsucht. Sollte es keinen passenden catch-Block geben, erfolgt nach dem zweiten Schritt sukzessive die Suche nach einer try-Anweisung in den umgebenden Blöcken.

Beispiel

static int div (Integer i1, Integer i2) {
  try {
    try {
      return i1.intValue()/i2.intValue();
    }
    catch (NullPointerException npe) {
      return 0;
    }
  }
  catch (ArithmeticException ae) {
    return Integer.MAX_VALUE;
  }
  /* finally { System.out.print("fin "); } */
}

Version ohne finally-Block:

Integer i= new Integer(7);
System.out.println(div(i,null));           // :: 0
System.out.println(div(i,new Integer(0))); // :: 2147483647
System.out.println(div(i,new Integer(2))); // :: 3

Version mit finally-Block : Vor jeder Zahl erfolgt die Ausgabe fin.


Galileo Computing

7.3.3 finally und unbehandelte Ausnahmen  downtop

Funktion von finally in der
try-Anweisung Icon

In einem finally-Block hat man keinen direkten Zugriff auf die ausgelöste Ausnahme. Allerdings kann man – meist aus Versehen – die Ausnahme durchaus entfernen oder mit einer anderen überschreiben. Wichtig ist die folgende Regel:

Unbehandelte Ausnahme

gp  Es kann nur jeweils eine unbehandelte Ausnahme von einer Methode bzw. einem Block nach außen übergeben werden.

Aufgrund dieser Regel wird eine Ausnahme, die im try-Block nicht behandelt wird, durch eine Ausnahme überschrieben, die im finally-Block ausgelöst und nicht behandelt wird.

Beispiele

finally-Problem:
verlorene Ausnahme!

Die Methode finallyTest1() erzeugt eine verlorene Ausnahme:

static void finallyTest1() throws Exception {
  try     { throw new Exception("try"); }
  finally { throw new Exception("fin"); }
}

Der Aufruf erzeugt nach der o.a. Regel:

try {       
  finallyTest1();
}
catch (Exception e) {
  System.out.println(e.toString()); // :: java.lang.Exception: 
fin
}

Die Variation finallyTest2() beseitigt das Problem der verlorenen Ausnahme:

finally-Variation mit innerem
try-catch

static void finallyTest2() throws Exception {
  try { throw new Exception("try"); }
  finally {
    try { throw new Exception("fin"); }
    catch (Exception e) { 
/* wird hier behandelt */ }
  }
}

return-Anweisung im finally-Block

In der folgenden Methode neverThrowsExceptions() hat die return-Anweisung in finally einen recht subtilen Nebeneffekt, der im zweiten Punkt des sechsten Schritts in 7.2.2 angesprochen wurde.

Icon
Wirkung von return im finally-Block

Es ist das Problem der verlorenen unbehandelten Ausnahme:

static void neverThrowsExceptions() throws Exception 
{
  try { throw new Exception(); }  // geht verloren!
  finally {
    return;   // Methode wird normal verlassen!
  }           // die Exception wurde implizit behandelt!
}
gp  Eine Methode, deren finally-Block eine return-Anweisung ausführt, liefert keine Ausnahmen nach außen.

Galileo Computing

7.3.4 Rethrowing in catch  downtop

Auch in catch-Blöcken können wieder neue Ausnahmen ausgelöst werden.

Rethrowing: Erneutes Auslösen derselben Ausnahme

gp  Unter Rethrowing versteht man die erneute Aktivierung derselben Ausnahme mittels throw, die dem catch-Block übergeben wurde.

Dies ist in Fällen sinnvoll, in denen catch – eventuell abhängig von bestimmten Bedingungen – die Ausnahme nur teilweise oder gar nicht behandeln kann und die Ausnahme deshalb weiterreichen muss.

Beispiel

In der Methode mean() wird eine Division durch 0 abgefangen (d.h., das Array hat keine Elemente). Wird mean() dagegen mit null aufgerufen, wird die Ausnahme nach außen weitergereicht.

Rethrowing einer RuntimeException

  public static int mean(int[] iarr) {
    try {
      int m= 0;
      for (int i=0; i<iarr.length; m+=iarr[i++]);
      return m/iarr.length;
    }
    catch (RuntimeException e) { 
      if (e instanceof NullPointerException) throw e;
      else return 0;
    }
  }

Hier drei verschiedene Aufrufe:

  System.out.println(mean(new int[] {1,2,3}));  
// :: 2
System.out.println(mean(new int[] {})); // :: 0
System.out.println(mean(null)); // NullPointerException

Galileo Computing

7.4 Ausnahme-Kategorien in Java  downtop

Das Basis-Package java.lang stellt bereits eine umfangreiche Klassen-Hierarchie von Ausnahmen bereit, die Auswirkungen auf den Compiler und die Art der Verwendung hat.


Galileo Computing

7.4.1 Hierarchie-Konzept  downtop

Icon

Throwable stellt die Basisklasse aller Ausnahmen dar (Abb. 7.2). Nach Java-Konvention sollen

Erzeugung von Ausnahmen

gp  von der Klasse Throwable – obwohl nicht abstrakt – keine eigenen Instanzen erzeugt werden und
gp  möglichst nur Ausnahmen von Leaf-Klassen erzeugt werden.

Begründet wird der letzte Punkt damit, dass die Hierarchie eine disjunkte Klassifikation der Laufzeitfehler darstellt, die den Fehler immer weiter präzisiert. Eine Leaf-Klasse bzw. -Instanz ist also informativer.

Ausnahme-Hierarchie: eine disjunkte Ausnahme-Kassifikation


Abbildung
Abbildung 7.2   Ausnahme-Kategorien

Icon

Nach Throwable folgen zwei große Kategorien von Fehlern:

Ausnahme-
Hierarchie

gp  Exception ist die Basisklasse für alle Ausnahmen, die im Code abgefangen und behandelt werden sollen.
gp  Error ist die Basisklasse für alle Fehler, die als nicht behebbar angesehen werden, also auch im Code nicht abgefangen werden sollen.

Mit anderen Worten: Error-Ausnahmen sollen tabu sein.


Galileo Computing

7.4.2 Checked vs. unchecked Exceptions  downtop

checked vs. unchecked
Exceptions

Die Begriffe checked bzw. unchecked Exceptions stehen für Ausnahmen, die im Code geprüft oder deklariert bzw. nicht geprüft werden müssen.

r Ausnahme-Klassen, die zu den checked Exceptions zählen, gilt folgende Regel:

Icon

Der Compiler stellt sicher, dass checked Exceptions

Compiler-Prüfung von checked Exceptions

gp  nur dann in Methoden nicht behandelt werden brauchen, wenn sie im Methoden-Kopf mittels throws deklariert sind:
   [mModifiers] ResultType methodName (parameterList)
                           throws Throwable1[,...ThrowableN]

Warnung zu
unchecked Exceptions

gp  in einer übergeordneten Methode entweder mittels try-catch abgefangen oder erneut im Kopf mittels throws deklariert werden.

Für unchecked Exceptions gibt es keine Regel, nur einen Warnhinweis:

gp  Diese Art von Ausnahmen kann zu jedem Zeitpunkt von der JVM oder explizit durch Code ausgelöst werden und führt dann ohne Behandlung immer zum Abbruch der Ausführung (Thread).

Icon

Separation: checked vs. unchecked

Error/RuntimeException sind unchecked

Zu den unchecked Exceptions gehören alle Error- und RuntimeException-Klassen bzw. -Subklassen, zu den checked Exceptions dann die restlichen (siehe Abb. 7.2).


Der Vorteil des Konzepts von checked Exceptions liegt in ihrer expliziten Natur. Man erkennt sie sofort bei Verwendung und hat nur die Alternative zwischen Behandeln und erneutem Weiterreichen.

Compiler-Prüfung

checked bzw. unchecked
Exception: Compiler vs. JVM

Die Unterscheidung der Ausnahme-Klassen in checked bzw. unchecked ist »magic«, d.h. kann nicht durch ein Marker-Interface erfolgen. Eo ipso wird – man ahnt es bereits – die Einhaltung der Checked-Exception-Regel nur vom Compiler geprüft.

gp  Die JVM kennt keine Unterschiede zwischen checked und unchecked Exceptions.

Galileo Computing

7.4.3 Basisklasse Throwable  downtop

Throwable:
nur logisch abstrakt

Ein Blick in die Implementation von Throwable zeigt – im Gegensatz zu dem suggestiven Interface-Namen – eine voll implementierte Klasse, die allen Subklassen wie Exception oder Error praktisch bereits die Arbeit abnimmt.

Die wohl wichtigste Eigenschaft von Throwable, und damit aller Subklassen; ist die Fähigkeit, bei der Instanzierung einer Ausnahme eine Fehlerbeschreibung anzugeben.

Java bietet dazu – sofern erwünscht – einen ausführlichen Execution Stack Trace, d.h. eine Lokalisierung des Ursprungs und seiner Proliferation durch den ausgeführten Code.

Da Throwable nicht selbst instanziert werden soll, erübrigt sich die Darstellung von Konstruktoren bzw. Methoden.

Error – »tödlicher« Laufzeitfehler


Galileo Computing

7.4.4 Ausnahmen vom Typ Error  downtop

Wie bereits in 7.4.1 erwähnt, signalisieren Fehler vom Typ Error: »Hände weg vom Programm«. Interessant ist nur, welche Fehler die Java-Entwickler darunter verstehen.

Die größte Gruppe, Ausnahmen vom Typ LinkageError, können zwar nicht im Programm-Code behandelt werden, müssen aber darauf untersucht werden, warum eine Klasse zur Laufzeit nicht eingebunden werden kann. Die häufigste Ausnahme NoClassDefFoundError kennt dagegen jeder Java-Programmierer.

Ausnahmen vom Typ VirtualMachineError drücken Fehler innerhalb der JVM aus, wobei OutOfMemoryError und StackOverflowError »von außen« beeinflusst werden können.

Die Klasse ThreadDeath gehört zwar logisch nicht zum Typ Error, wurde hier aber platziert, um nicht mit einem catch(Exception e) versehentlich abgefangen zu werden.

Diese Art der Ausnahme wird durch den mutwilligen Thread-Tod mittels stop() ausgelöst und erzeugt – im Gegensatz zu anderen – keine Meldung. Man kann ihn also schlichtweg ignorieren.


Galileo Computing

7.4.5 Ausnahmen vom Typ Exception  downtop

Java-Konvention zum Typ Exception

Alle für den Code interessanten Ausnahmen sind vom Typ Exception, da sie entweder im Programm behandelt werden müssen oder fehlerhaften Code signalisieren, der verbessert werden muss.

Icon

Nach Java-Konvention

Deklaration und Benennung

gp  enden alle Namen von Ausnahmen dieses Typs mit Exception.
gp  werden eigene Ausnahme-Klassen als Subklassen von Exception deklariert, und zwar normalerweise als checked Exception.

Galileo Computing

7.4.6 Ausnahmen vom Typ RuntimeException  downtop

RuntimeException
signalisiert einen Programmierfehler

Ausnahmen vom Typ RuntimeException signalisieren, einfach gesagt, Fehler im Code, die der Compiler leider nicht aufdecken konnte.

gp  Robuster Code muss so geschrieben werden, das Ausnahmen vom Typ RuntimeException nicht mehr auftreten.

So einfach diese Aussage, so schwierig ihre Umsetzung. Denn diese Fehler

gp  müssen nicht explizit abgefangen werden.
gp  werden häufig durch unzulässige Argumente beim Methodenaufruf ausgelöst.

Der zweite Punkt fällt dann unter das Thema Kontraktmanagement.

Icon

Gerade deshalb werden in der Tabelle 6.1 zu den häufigsten RuntimeExceptions exemplarisch einfache Vermeidungsstrategien angegeben.

Vermeidung von RuntimeExceptions

Tabelle 7.1   Vermeidungsstrategien zu RuntimeExceptions
RuntimeException Wird verhindert durch:
ArithmeticException Vor div- bzw. mod-Operationen int-Operanden vorher prüfen
IndexOutOfBoundsException String- oder Array-Index prüfen
ClassCastException Mit instanceof-Operator den Typ prüfen
NullPointerException Referenz auf nicht null prüfen
NumberFormatException Konvertierungen von Strings nach numerische Typen in try-catch einbetten


Galileo Computing

7.4.7 Checked Exceptions  downtop

Aufgrund des Lazy-Programmer-Syndroms fallen die meisten Ausnahmen in den Packages der Java-Plattform und anderer Anbieter unter die Kategorie »checked Exceptions«.

Checked Exceptions: unvermeidliche Umgebungsfehler

Im Gegensatz zu RuntimeException enthält diese Kategorie Fehler, die durch die Programmumgebung und nicht durch eigenen fehlerhaften Code verursacht werden.

Hierunter fallen u.a. Klassen, die nicht (mehr) gefunden werden, Ein- und Ausgabefehler, Kommunikations-, Netzwerk- oder andere Hardwareprobleme.

Insbesondere diese Fehler müssen in einem robusten Programm unbedingt abgefangen werden, und es müssen – je nach Aufgabe – Strategien implementiert werden, wie diesen Fehlern zu begegnen ist.


Galileo Computing

7.5 Overriding von Ausnahmen  downtop

Icon

Beim Design von Methoden in einer Basisklasse bzw. einem Interface muss berücksichtigt werden, ob ein Überschreiben dieser Methode eventuell eine checked Exceptions notwendig macht. Denn es gilt die Regel:

Overriding bzw. Überschreiben von Ausnahmen

gp  Overriding einer Methode, die eine checked Exception deklariert, kann nur durch eine Methode erfolgen, die alternativ
    gp  Ausnahmen vom selben Typ oder Subtyp oder
    gp  keine Ausnahmen

deklariert.

Erklärung: Diese »einengende« Vererbungsrestriktion musste für Klassen und Interfaces eingeführt werden. Der Grund liegt in der Kombination aus Substitutions- und Checked-Exception-Regel:

Denn ohne die Overriding-Regel könnten Instanzen einer Subklasse eine Ausnahme auslösen, die in der Superklasse nicht deklariert ist.

Eingesetzt in Code, der nur Ausnahmen der Superklasse abfangen muss, würden sie dann locker die Regel für checked Exceptions (siehe 7.4.2) aushebeln.

Beispiele

Overriding von Ausnahmen bei Vererbung

interface I { void f() /* throws Exception */ ; 
}
class A     { void f() throws Exception{} }
// C-Fehler, da f() in I keine Exception deklariert
class B extends A implements I {
  public void f() throws Exception {} 
}

Wird im Interface I die Methode f() analog zu A deklariert, gibt es aufgrund der Regel keinen Compiler-Fehler in Klasse B.

In den nächsten zwei Beispielen wird die Ausnahme-Hierarchie des Packages java.io verwendet (siehe Abb. 7.3).


Abbildung
Abbildung 7.3   Ausschnitt aus der Exception-Hierarchie von java.io
interface I { void f() throws FileNotFoundException; 
}
class A { void f() throws EOFException{} }
class B extends A implements I {
  // die nachfolgenden Deklarationen sind nicht 
möglich
  public void f() throws FileNotFoundException{} // C-Fehler
  public void f() throws EOFException{}          // C-Fehler
  public void f() throws FileNotFoundException,
                         EOFException {}         // C-Fehler
  // einer der nachfolgenden Deklarationen ist möglich
  public void f() {}                             // ok!
  public void f() throws Error {}                // auch ok!
}

Siehe hierzu die o.a. Regeln.

Implementierungs-Hierarchie

Auch bei Hierarchien gilt die Overriding-Regel für Ausnahmen, wie das nächste Beispiel zeigt:

interface I1            { void f() throws IOException; 
}
interface I2 extends I1 { 
  void f() throws ObjectStreamException; 
}
class B implements I2 {
  // nur noch (Sub-)Typen von ObjectStreamException möglich
  public void f() throws InvalidClassException {}
}

Die Klasse B muss sich an Interface I2 orientieren.

Icon

Abschließend die Konsequenz, formuliert als Design-Regel:

Notwendige Ausnahmen in Basisklassen

gp  Methoden in Basisklassen bzw. Interfaces müssen bereits die checked Exceptions deklarieren, die in Subklassen benötigt werden.


Galileo Computing

7.6 Deklaration neuer Ausnahmen  downtop

Die Standard-Plattform enthält eine unüberschaubare Zahl von speziellen Ausnahme-Klassen, auf die man – sofern sie passen – zurückgreifen kann.

Natürlich kann man in einem eigenen Package – analog zu java.io – auch eigene Ausnahme-Klassen deklarieren. Ob man eine checked oder unchecked Exception deklariert, soll erst im folgenden Abschnitt behandelt werden. Zuerst interessiert der Mechanismus.

Icon
Muster für die Deklaration neuer Ausnahmen

   class NewException extends ExistingException 
{
     public NewException () {
        // evtl. Aufruf von super(...);
     }
     public NewException (String description) {
       super(description);  // explizite Fehler-Beschreibung
     }
   }

Ist ExistingException eine unchecked Exception, ist dies NewException ebenfalls.

Mit super(description) wird eine Fehlerbeschreibung bis nach Throwable durchgereicht, der einen entsprechenden Konstruktor bereitstellt:

public class Throwable implements java.io.Serializable {
  public Throwable(String message) { 
 fillInStackTrace(); detailMessage = message;
  }
  //...
}

Aufgrund des Marker-Interfaces Serializable können alle Ausnahmen gespeichert werden.10 

Beispiel (Umwandlung einer Exception)10 

Deklaration neuer Ausnahmen

Eine gültige Teile-Nummer besteht aus maximal zehn Stellen und enthält nur Ziffern oder Bindestriche.11  Es soll eine PrimaryKeyException im Konstruktor ausgelöst werden, wenn die Teile-Nummer ungültig ist. Diese soll explizit abgefangen werden.

class PrimaryKeyException extends Exception {   
 // checked!
public PrimaryKeyException () { super("Fehlerhafter Teile-Primärschlüssel!"); } public PrimaryKeyException (String description) { super(description); } }
class Teil {
  String nr;
  Teil(String nr) throws PrimaryKeyException {
    if (nr.length()>10) throw new PrimaryKeyException();
    try { Long.parseLong(nr.replace('-','0')); }
    catch (NumberFormatException e) {  // Umwandlung einer
      throw new PrimaryKeyException(); // Runtime-Exception
    }
    this.nr=nr;
  }
  //...
}

Galileo Computing

7.7 Einsatz von Ausnahmen  downtop

Bisher wurden Ausnahme-Mechanismen anhand bestehender Hierarchien vorgestellt und gezeigt, wie man korrekt reagiert.

Aber die eigentlich schwierig Frage, wie Ausnahmen in eigenen Applikationen sinnvoll und effizient eingesetzt werden, ist noch unbeantwortet.

Soviel vorab: Es gibt keine Ausnahme-Pattern, jedoch durchaus Idiome. Die Ausnahme-Behandlung ist mit anderen Worten an die jeweilige Sprache gebunden, und Antworten auf Fragen zum optimalen Ausnahme-Design bewegen sich gerade in Java auf dünnem Eis.12 

Icon


Galileo Computing

7.7.1 Missbrauch von Ausnahmen  downtop

Zum Einstieg ein Hinweis:

Ausnahmen als
Ersatz für andere Kontrollstrukturen?

gp  Ausnahmen dienen nicht als Ersatz für andere Kontrollstrukturen.

Das folgende Code-Segment ist ein extremes Beispiel:

double[] val= {1.,2.,3.};
double sum=0.;
try {
  int i=0;
  while (true)
    sum+= val[i++]; 
}
catch(Exception e) {}     // ignoriert Index-Überschreitung!
System.out.println(sum);  // :: 6.0

Natürlich sind die Dinge nicht immer so klar wie in diesem Beispiel.


Galileo Computing

7.7.2 Ausnahmen zur Einhaltung von Kontrakten  downtop

Assertion – Kontrakt

Unter Kontrakten – auch Assertions genannt – versteht man formale Eigenschaften, die vom Client einer Methode eingehalten werden müssen oder auch von der Methode selbst (siehe auch 6.12.1).

Eine häufige Kontraktverletzung besteht darin, die Preconditions (Vorbedingungen) beim Aufruf einer Methode nicht einzuhalten. Deshalb:

Ausnahmen: Einsatz bei
Kontrakt- Missachtung

gp  Robuste Methoden erzwingen die Einhaltung ihrer Preconditions mit Hilfe von Ausnahmen.

Diese Regel ist nicht ganz so trivial in der Umsetzung, denn es gibt Alternativen.

Default-Werte
bei Kontraktverletzung

Default-Alternative: Werden Vorbedingungen nicht eingehalten, kann man mit Hilfe von Default-Werten die Operationen durchführen. Dies muss für den Client allerdings klar erkennbar sein, da er ansonsten von den Ergebnissen sehr überrascht sein könnte.

Ausnahme-Auswahl: Zur Wahl stehen checked vs. unchecked Exceptions. Beim Typ RuntimeException ist dem Client freigestellt, ob er mittels try-catch fehlerhafte Argument abfängt oder ob er darauf verzichten will. Im anderen Fall lässt man ihm keine Wahl (siehe 7.7.3).

Kontrakt-Beispiel: Konstruktor

Ausnahmen im Konstruktor

Fehlerhafte Argumente bei Konstruktoren durch Default-Werte zu ersetzen, ist nicht gerade genial. Wozu ist wohl ein No-Arg-Konstruktor da?

Ohne die Hilfe von Ausnahmen kann sich ein Konstruktor nicht wirksam gegen fehlerhafte Aufrufe wehren, da konventionelle Fehlermitteilungen durch Rückgabewerte entfallen.

Icon
Ausnahme verhindert Instanz-Anlage

gp  Bei Auslösen einer Ausnahme in einem Konstruktor wird die Instanz nicht angelegt, und eine Referenz auf die Instanz hat weiterhin den alten Wert.13  14 

Beispiel14 

class Teil {
  static final String mes= "Fehlerhafter Teile-Schlüssel!";
  String nr;
  // throws nicht notwendig, aber eine explizite 
Drohung!
  Teil(String nr) throws IllegalArgumentException {
    if (nr.length()>10) throw new IllegalArgumentException(mes);
    try { Long.parseLong(nr.replace('-','0')); }
    catch (NumberFormatException e) {
      throw new IllegalArgumentException(mes);
    }
    this.nr=nr;
  }
}

Der Vorteil einer Unchecked-Variante liegt beim Client, der nun die Wahl hat:

public class Test {
  public static void main(String[] args) {
    String nr="234-x";
    Teil t= null;
    try { t= new Teil(nr); }
    catch (RuntimeException re) {
      System.out.println(t==null);             // :: true
    }
    // ohne try Programm-Abbruch mit Ausnahme
    new Teil(nr);         
  }
}

Galileo Computing

7.7.3 Kommunikation per Ausnahme  downtop

Die JVM unterscheidet überhaupt keine Ausnahmen, der Java-Compiler kennt nur checked und unchecked Exceptions, eine hilfreiche, aber viel zu grobe Unterteilung.

Ausnahmen
differenziert nach Kommunikationsart


Abbildung
Abbildung 7.4   Ausnahme-Klassifikation anhand der Kommunikationsart

Die Kommunikation zwischen Sender (Server) und Empfänger (Client) mittels Ausnahmen sollte man ein wenig genauer differenzieren (siehe Abb. 7.4).

Kommunikationssignal

Kommunikations-signal

Ausnahmen, die zu einer Unterbrechung des normalen Programmablaufs führen, können auch Kommunikations-Signale sein:

Ausnahme als Interrupt-Objekt

gp  Begreift man eine Ausnahme als besonderes Signal15  des Senders, dann ist es ein Interrupt-Objekt, welches den normalen Programmablauf eines eventuell unbekannten Empfängers unterbricht.

Spätestens bei den Threads wird klar, das Ausnahmen als Signale auch zu Kommunikationsaufgaben herangezogen werden, was aber leider nur sehr ungenügend von der Java-Sprache unterstützt wird.

Fehler im Code

Ausnahme:
Signalisieren
von Code- oder Umgebungsfehler

Fehler-Signale können – der Interpretation der Java-Entwickler folgend – in Fehler im Code und Umgebungsfehler unterschieden werden

Fehler im Code müssen letztendlich entfernt werden (siehe 7.4.6).

Icon

Hat man für diese Art von Laufzeitfehlern checked Exceptions gewählt, steht man anschließend vor vielen try-catch-Anweisungen, die den Code sinnlos aufblähen. Dies führt zum ersten Separations-Idiom.

RuntimeException: geeignet für Fehler im Code

Exception-Separation: Unterscheide Fehler im Code von Umgebungsfehlern und signalisiere Codierungsfehler mit Hilfe einer Subklasse von RuntimeException.

Die Nichteinhaltung von Kontrakten fällt häufiger unter die Rubrik »Fehler im Code«. Ein unzulässiges null-Argument kann durchaus mit einer NullPointerException geahndet werden. Ein Client hat ja die Wahl, den Kontrakt einzuhalten.

Umgebungsfehler

RuntimeException: ungeeignet für Umgebungsfehler

Umgebungsfehler dürfen nicht ignoriert werden. Hierfür ist eine anonyme RuntimeException nicht sinnvoll, da man bei der Benutzung des Services die try-catch-Anweisungen nicht weglassen darf.

Normale Reaktion: Die Server-Methode erklärt die Ausnahme im Kopf der Methode mittels throws als Subtyp von Exception (nicht Runtime!) und erwartet, dass sich der Client um das Problem kümmert.

Error: tötlicher Umgebungsfehler

Harte Reaktion: Die Server-Methode löst eine Ausnahme vom Typ Error aus. Als Resultat stirbt wahrscheinlich auch der Client, da dieser mit catch(Exception e) {...} keinen Error abfangen kann.

Fatale vs. nicht fatale Fehler

Fatale Fehler:
der Client
entscheidet

Der Client unterscheidet die bei ihm eintreffenden Ausnahmen in fatale Fehler, die ihn seinerseits zum Abbruch zwingen, und in nicht fatale.

gp  Was fatal ist, bestimmt der Client.

Nicht fatale Fehler:
Wiederholen/Ignorieren

gp  Nicht fatale Fehler können mit der Strategie »Wiederholen« (mit/ohne Variation) oder »Ignorieren« behandelt werden.

Ein gescheiterter Verbindungsaufbau oder Sperrversuch eines Datensatzes führt zwar zu einer Ausnahme, aber eventuell erst nach mehrmaliger Wiederholung zu einem fatalen Fehler.

In einer Multimedia-Anwendung mag es Laufzeitfehler geben, die aber bis zu einem Grenzwert ignoriert werden müssen.16  Dies führt zum zweiten Separations-Idiom.